#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/sched/signal.h>

#define GLOBALFIFO_SIZE 0x1000
#define MEM_CLEAR 0x1

static dev_t devno = 0;
static struct class* globalfifo_cls;

struct globalfifo_dev
{
    struct cdev c_dev;
    unsigned char mem[GLOBALFIFO_SIZE];
    struct mutex mtx;
    int current_len;
    wait_queue_head_t r_wait;
    wait_queue_head_t w_wait;
};
static struct globalfifo_dev* globalfifo_devp;

static int globalfifo_open(struct inode* inode, struct file* filp)
{
    filp->private_data = globalfifo_devp;
    return 0;
}

static int globalfifo_release(struct inode* inode, struct file* filp)
{
    return 0;
}

static long globalfifo_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
{
    struct globalfifo_dev* dev = filp->private_data;

    switch (cmd)
    {
    case MEM_CLEAR:
        mutex_lock(&dev->mtx);
        memset(dev->mem, 0, GLOBALFIFO_SIZE);
        mutex_unlock(&dev->mtx);
        printk(KERN_INFO "globalfifo is set to zero\n");
        break;
    default:
        return -EINVAL;
        break;
    }

    return 0;
}

static ssize_t globalfifo_read(struct file* filp, char __user* buf, size_t size, loff_t* ppos)
{
    unsigned long p = *ppos;
    unsigned int count = size;
    int ret = 0;
    struct globalfifo_dev* dev = filp->private_data;
    DECLARE_WAITQUEUE(wait, current);   //struct wait_queue_entry wait = current

    mutex_lock(&dev->mtx);
    add_wait_queue(&dev->r_wait, &wait);    //add current thread into r_wait list

    while (dev->current_len == 0)
    {
        if (filp->f_flags & O_NONBLOCK)
        {
            ret = -EAGAIN;
            goto out;
        }

        __set_current_state(TASK_INTERRUPTIBLE);
        mutex_unlock(&dev->mtx);

        schedule();
        if (signal_pending(current))    //thread is waked up by signal, not data
        {
            ret = -ERESTARTSYS;
            goto out2;
        }

        mutex_lock(&dev->mtx);
    }

    if (count > GLOBALFIFO_SIZE - p)
    {
        count = GLOBALFIFO_SIZE - p;
    }
    if (count > dev->current_len)
    {
        count = dev->current_len;
    }

    if (copy_to_user(buf, dev->mem + p, count))
    {
        ret = -EFAULT;
    }
    else
    {
        memcpy(dev->mem, dev->mem + count, dev->current_len - count);
        dev->current_len -= count;
        printk(KERN_INFO "read %u bytes from %lu, current_len %d\n", count, p, dev->current_len);
        wake_up_interruptible(&dev->w_wait);    //wake up threads waiting to write
        ret = count;
    }

out:
    mutex_unlock(&dev->mtx);
out2:
    remove_wait_queue(&dev->r_wait, &wait);
    set_current_state(TASK_RUNNING);

    return ret;
}

static ssize_t globalfifo_write(struct file* filp, const char __user* buf, size_t size, loff_t* ppos)
{
    unsigned long p = *ppos;
    unsigned int count = size;
    int ret = 0;
    struct globalfifo_dev* dev = filp->private_data;
    DECLARE_WAITQUEUE(wait, current);

    mutex_lock(&dev->mtx);
    add_wait_queue(&dev->w_wait, &wait);

    while (dev->current_len == GLOBALFIFO_SIZE)
    {
        if (filp->f_flags & O_NONBLOCK)
        {
            ret = -EAGAIN;
            goto out;
        }

        __set_current_state(TASK_INTERRUPTIBLE);
        mutex_unlock(&dev->mtx);

        schedule();
        if (signal_pending(current))
        {
            ret = -ERESTARTSYS;
            goto out2;
        }

        mutex_lock(&dev->mtx);
    }

    if (count > GLOBALFIFO_SIZE - p)
    {
        count = GLOBALFIFO_SIZE - p;
    }
    if (count > GLOBALFIFO_SIZE - dev->current_len)
    {
        count = GLOBALFIFO_SIZE - dev->current_len;
    }


    if (copy_from_user(dev->mem + dev->current_len, buf, count))
    {
        ret = -EFAULT;
    }
    else
    {
        dev->current_len += count;
        printk(KERN_INFO "written %u bytes from %lu, current_len %d\n", count, p, dev->current_len);
        wake_up_interruptible(&dev->r_wait);    //wake up thread waiting to read
        ret = count;
    }

out:
    mutex_unlock(&dev->mtx);
out2:
    remove_wait_queue(&dev->w_wait, &wait);
    set_current_state(TASK_RUNNING);

    return ret;
}

static loff_t globalfifo_llseek(struct file* filp, loff_t offset, int orig)
{
    loff_t ret = 0;
    switch (orig)
    {
    case SEEK_SET:      //beginning
        if (offset < 0)
        {
            ret = -EINVAL;
            break;
        }
        if (offset > GLOBALFIFO_SIZE)
        {
            ret = -EINVAL;
            break;
        }
        filp->f_pos = offset;
        ret = filp->f_pos;
        break;
    case SEEK_CUR:
        if (filp->f_pos + offset > GLOBALFIFO_SIZE)
        {
            ret = -EINVAL;
            break;
        }
        if (filp->f_pos + offset < 0)
        {
            ret = -EINVAL;
            break;
        }
        filp->f_pos += offset;
        ret = filp->f_pos;
        break;
    default:
        ret = -EINVAL;
        break;
    }

    return ret;
}

static const struct file_operations globalfifo_fops =
{
    .owner = THIS_MODULE,
    .llseek = globalfifo_llseek,
    .read = globalfifo_read,
    .write = globalfifo_write,
    .unlocked_ioctl = globalfifo_ioctl,
    .open = globalfifo_open,
    .release = globalfifo_release,
};

static void globalfifo_setup_cdev(struct globalfifo_dev* dev, int index)
{
    int err;
    struct device* globalfilo_device;

    cdev_init(&dev->c_dev, &globalfifo_fops);
    dev->c_dev.owner = THIS_MODULE;
    err = cdev_add(&dev->c_dev, devno, 1);

    if (err)
    {
        printk(KERN_NOTICE "Error %d adding globalfifo%d\n", err, index);
    }
    globalfifo_cls = class_create(THIS_MODULE, "globalfifo");
    globalfilo_device = device_create(globalfifo_cls, NULL, devno, NULL, "globalfifo");
}

static int __init globalfifo_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo");
    if (ret < 0)
    {
        return ret;
    }

    globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
    if (!globalfifo_devp)
    {
        ret = -ENOMEM;
        goto fail_malloc;
    }

    mutex_init(&globalfifo_devp->mtx);
    init_waitqueue_head(&globalfifo_devp->r_wait);
    init_waitqueue_head(&globalfifo_devp->w_wait);

    globalfifo_setup_cdev(globalfifo_devp, 0);
    return 0;

fail_malloc:
    unregister_chrdev_region(devno, 1);
    return ret;
}
module_init(globalfifo_init);

static void __exit globalfifo_exit(void)
{
    device_destroy(globalfifo_cls, devno);
    class_destroy(globalfifo_cls);
    cdev_del(&globalfifo_devp->c_dev);
    kfree(globalfifo_devp);
    unregister_chrdev_region(devno, 1);
}
module_exit(globalfifo_exit);

MODULE_LICENSE("GPL");
